A demanda energética trata da necessidade de energia requerida pela população em diversores setores da sociedade. As industrias utilizarão energia para produzir produtos. O comércio consumirá mais energia nas lojas onde esses produtos serão vendidos e nas casas das pessoas que os ultilizará.
A previsão do consumo de energia é de suma importância para o planejamento do país, pois deve-se considerar como toda essa energia será produzida e como ela chegará a todas as pessoas em cada lugar do Brasil. Precisa-se considerar, por exemplo, o quanto as fontes de energia que temos disponíveis no Brasil variam ao longo do dia ou do ano. Além disso, precisam ser construídas linhas de transmissão, responsáveis por levar a eletricidade das usinas paras as cidades.
Neste projeto, utilizaremos a biblioteca Prophet para prever dados de série temporal da demanda de energia elétrica pela região Nordeste do Brasil.
Fonte: EPE, s.d.
# Importação de bibliotecas
import pandas as pd
import plotly as pl
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.seasonal import seasonal_decompose
import numpy as np
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from pandas.plotting import autocorrelation_plot
import warnings
from statsmodels.tools.sm_exceptions import ConvergenceWarning
warnings.simplefilter('ignore', ConvergenceWarning)
from statsmodels.tsa.stattools import adfuller
from math import sqrt
import plotly.express as pl
import pickle
from statsmodels.tsa.stattools import adfuller, kpss
from statsmodels.graphics.tsaplots import acf, pacf
import plotly.graph_objs as go
from prophet import Prophet
from prophet.diagnostics import performance_metrics
from scipy.stats import boxcox
from scipy.special import inv_boxcox
from prophet.diagnostics import cross_validation
from fbprophet.plot import plot_cross_validation_metric
import itertools
from prophet.plot import plot_plotly, plot_components_plotly
O conjunto de dados contém o consumo histórico (em GWh), por mês, de Janeiro de 1979 até Março de 2021, por clientes da região Nordeste. Os dados foram obtidos do site ipeadata. O Ipeadata é uma base de dados macroeconômicos, financeiros e regionais do Brasil mantida pelo Ipea (Instituto de Pesquisa Econômica Aplicada).
# obtenção dos dados
df = pd.read_csv(r'C:\Users\Andre\Downloads\ipeadata[19-06-2021-08-55].csv')
df
# separando a coluna em data e consumo
df = df['Data;Consumo - energia elétrica - Região Nordeste (NE) - qde. - GWh - Eletrobras - ELETRO12_CEENE12;'].str.split(';', expand=True)
# excluindo a coluna vazia gerada
df = df.drop(columns=[2])
# renomeando as colunas
df = df.rename(columns={0: "data", 1: "consumo (GWh)"})
# usando a função to_datetime para formatar as datas
df['data'] = pd.to_datetime(df['data'], errors='coerce')
# convertendo a coluna 'consumo (GWh)' para númerico
df['consumo (GWh)'] = pd.to_numeric(df['consumo (GWh)'])
# transformando a coluna 'data' em índice
df.set_index('data', inplace=True)
# visualizando o dataframe
df
# visualizando gráfico do conjunto de dados
df['mean'] = df['consumo (GWh)'].rolling(12).mean()
fig = go.Figure()
fig.add_trace(go.Scatter(x = df.index, y = df['mean'], name = 'Média',))
fig.add_trace(go.Scatter(x = df.index, y = df['consumo (GWh)'], name = 'Consumo',))
fig.show()
É possível observar que a série temporal possui tendência de crescimento. Isso pode ser explicado pelo aumento da população, segundo o IBGE, em 1979 a população do nordeste era de cerca de 35 milhôes de pessoas¹, em 2020 a população é de cerca de 57 milhões de pessoas², ainda de acordo com o IBGE.
Ainda observa-se que entre os meses de Maio à Julho de 2001, há uma grande queda no consumo de energia, isso é justificado pelo corte de energia programado para conter o consumo e evitar colapso no abastecimento de energia elétrica em todo país, devido ao atraso em obras de transmissão e geração de energia, segundo relatório escrito pela Comissão de Análise do Sistema Hidrotérmico de Energia Elétrica, em julho de 2001³.
# plotando gráfico do tempo a partir do ano de 2014 com a média móvel
filtro = (df.index.year >= 2014)
df[filtro].plot(figsize = (15, 5))
Verificando o gráfico a partir do ano de 2014, observa-se certa estagnação no consumo de energia. Isso é atribuída à recessão econômica que enfrenta o país⁴. Ainda, verifica-se uma maior queda em 2020 devido a crise causada pela pandemia do Covid-19.
Como o consumo de energia está relacionado diretamente pelo desevolvimento econômico do país, a partir deste ponto, utilizaremos os dados a partir do ano de 2002. Pois, anteriormente, os dados fornecidos tem relação apenas quanto ao crescimento da população, e não haverá grande influência para o objeto de estudo deste projeto.
# Utilizando os dados a partir do ano de 2002
df = df[df.index > '2002']
# plotando gráfico de barras com diferenciação e média por ano para análise
df['consumo (GWh)'].diff().groupby(df.index.year).mean().plot(kind='bar', figsize = (15, 5))
No gráfico acima fica claro o baixo consumo de energia entre os anos de 2015 e 2018 e a grande queda em 2020 precedido pelo aumento de consumo em 2019.
# plotando gráfico de barras com diferenciação e média por mês para análise
df['consumo (GWh)'].diff().groupby(df.index.month).mean().plot(kind='bar', figsize = (15, 5))
O gráfico acima mostra que em média há uma queda de consumo nos meses de Fevereiro, Abril e Junho, com aumento no consumo em Março, Agosto e Outubro.
# excluindo a coluna média
df = df.drop(columns='mean')
# excluindo valores ausentes
df = df.dropna()
# visualizando a tendência, sazionalidade e resíduos dos dados
result = seasonal_decompose(df['consumo (GWh)'])
# configurando o tamanho do gráfico
fig, (ax1,ax2,ax3, ax4) = plt.subplots(4,1, figsize=(12,8))
result.observed.plot(ax=ax1)
result.trend.plot(ax=ax2)
result.seasonal.plot(ax=ax3)
result.resid.plot(ax=ax4)
plt.tight_layout()
# criando cópia do dataframe
df_copy = df
Em séries temporais, geralmente é útil fazer alguma forma de transformação de potência dos dados para estabilizar a variância e tornar os dados mais semelhantes a uma distribuição normal. Usaremos uma transformação Box Cox que avalia um conjunto de coeficientes lambda (λ) e seleciona o valor que atinge a melhor aproximação da normalidade.
# Aplicando transformação Box-Cox e salvando o valor de lambda para realizar posterior inversão
df_copy['consumo (GWh)'], lam = boxcox(df_copy['consumo (GWh)'])
print('Lambda:', lam)
# vizualizando gráfico com a série trasnformada
fig = pl.line(df_copy, x=df_copy.index, y= df_copy['consumo (GWh)'])
fig.show()
# visualizando o histograma dos dados para verificar a distribuição
df_copy['consumo (GWh)'].plot(kind='kde')
plt.show()
A biblioteca Prophet é uma ferramenta open source lançada pelo Facebook em 2017. A ferramenta visa contribuir para problemas de geração de previsões e cenários futuros para séries temporais. Veja as características do Prophet (em que ele é melhor, segundo o site oficial⁵):
Para modelar séries temporais, o Prophet separa o sinal nos seguintes componentes aditivos: tendêndia, sazionalidade e feriados. De acordo com a formula abaixo:
Onde:
g(t) é a função de tendência que modela mudanças não periódicas no valor da série temporal;
s(t) representa mudanças periódicas (por exemplo, sazionalidade semanal ou anual)
h(t) representa o efeito de feriados que ocorrem em programações potencialmente irregulares ao longo de um ou mais dias.
εₜ é é o erro / ruído do modelo, que é considerado normalmente distribuído.
# formatando o dataframe para o padrão da biblioteca prophet
df_prophet = df_copy.reset_index()
df_prophet = df_prophet.rename(columns={'data':'ds', 'consumo (GWh)':'y'})
# visulizando o dataframe formatado
df_prophet.head()
Abaixo é criado um dataframe com os principais feriados do Brasil para ser implementado no governo.
# criando dataframe com feriados do Brasil
ano_novo = pd.DataFrame({
'holiday': 'ano_novo',
'ds': pd.to_datetime(['2002-01-01', '2003-01-01', '2004-01-01',
'2005-01-01', '2006-01-01', '2006-01-01',
'2007-01-01', '2008-01-01', '2009-01-01',
'2010-01-01', '2011-01-01', '2012-01-01',
'2013-01-01', '2014-01-01', '2015-01-01',
'2016-01-01', '2017-01-01', '2018-01-01',
'2019-01-01', '2020-01-01', '2021-01-01']),
})
pascoa = pd.DataFrame({
'holiday': 'pascoa',
'ds': pd.to_datetime(['2002-04-05', '2003-04-05', '2004-04-05',
'2005-04-05', '2006-04-05', '2006-04-05',
'2007-04-05', '2008-04-05', '2009-04-05',
'2010-04-05', '2011-04-05', '2012-04-05',
'2013-04-05', '2014-04-05', '2015-04-05',
'2016-04-05', '2017-04-05', '2018-04-05',
'2019-04-05', '2020-04-05', '2021-04-05']),
})
tiradentes = pd.DataFrame({
'holiday': 'tiradentes',
'ds': pd.to_datetime(['2002-04-21', '2003-04-21', '2004-04-21',
'2005-04-21', '2006-04-21', '2006-04-21',
'2007-04-21', '2008-04-21', '2009-04-21',
'2010-04-21', '2011-04-21', '2012-04-21',
'2013-04-21', '2014-04-21', '2015-04-21',
'2016-04-21', '2017-04-21', '2018-04-21',
'2019-04-21', '2020-04-21', '2021-04-21']),
})
trabalho = pd.DataFrame({
'holiday': 'trabalho',
'ds': pd.to_datetime(['2002-05-01', '2003-05-01', '2004-05-01',
'2005-05-01', '2006-05-01', '2006-05-01',
'2007-05-01', '2008-05-01', '2009-05-01',
'2010-05-01', '2011-05-01', '2012-05-01',
'2013-05-01', '2014-05-01', '2015-05-01',
'2016-05-01', '2017-05-01', '2018-05-01',
'2019-05-01', '2020-05-01', '2021-05-01']),
})
independencia = pd.DataFrame({
'holiday': 'independencia',
'ds': pd.to_datetime(['2002-09-07', '2003-09-07', '2004-09-07',
'2005-09-07', '2006-09-07', '2006-09-07',
'2007-09-07', '2008-09-07', '2009-09-07',
'2010-09-07', '2011-09-07', '2012-09-07',
'2013-09-07', '2014-09-07', '2015-09-07',
'2016-09-07', '2017-09-07', '2018-09-07',
'2019-09-07', '2020-09-07', '2021-09-07']),
})
finados = pd.DataFrame({
'holiday': 'finados',
'ds': pd.to_datetime(['2002-11-02', '2003-11-02', '2004-11-02',
'2005-11-02', '2006-11-02', '2006-11-02',
'2007-11-02', '2008-11-02', '2009-11-02',
'2010-11-02', '2011-11-02', '2012-11-02',
'2013-11-02', '2014-11-02', '2015-11-02',
'2016-11-02', '2017-11-02', '2018-11-02',
'2019-11-02', '2020-11-02', '2021-11-02']),
})
republica = pd.DataFrame({
'holiday': 'republica',
'ds': pd.to_datetime(['2002-11-15', '2003-11-15', '2004-11-15',
'2005-11-15', '2006-11-15', '2006-11-15',
'2007-11-15', '2008-11-15', '2009-11-15',
'2010-11-15', '2011-11-15', '2012-11-15',
'2013-11-15', '2014-11-15', '2015-11-15',
'2016-11-15', '2017-11-15', '2018-11-15',
'2019-11-15', '2020-11-15', '2021-11-15']),
})
natal = pd.DataFrame({
'holiday': 'natal',
'ds': pd.to_datetime(['2002-12-25', '2003-12-25', '2004-12-25',
'2005-12-25', '2006-12-25', '2006-12-25',
'2007-12-25', '2008-12-25', '2009-12-25',
'2010-12-25', '2011-12-25', '2012-12-25',
'2013-12-25', '2014-12-25', '2015-12-25',
'2016-12-25', '2017-12-25', '2018-12-25',
'2019-12-25', '2020-12-25', '2021-12-25']),
})
holidays = pd.concat((ano_novo, pascoa, tiradentes, trabalho, independencia, finados, republica, natal))
# visualizando o dataframe dos feriados
holidays
# instanciando a biblioteca Prophet
m = Prophet(holidays=holidays)
A biblioteca Prophet permitel dividir os dados históricos em dados de treinamento e dados de teste para validação cruzada. Os principais conceitos para validação cruzada com o Profeta são:
Adicionalmente, testaremos diferentes hiperparâmetros em um loop e, em seguida, obteremos os melhores, com o objetivo de melhorar o modelo. Esse processo é relativamente custoso computacionalmente e leva alguns minutos para ser concluído.
# utilizando a técnica de validação cruzada para obter os melhores parâmetros do modelo
param_grid = {
'changepoint_prior_scale': [0.001, 0.01, 0.1, 0.5],
'seasonality_prior_scale': [0.01, 0.1, 1.0, 10.0],
}
# Generate all combinations of parameters
all_params = [dict(zip(param_grid.keys(), v)) for v in itertools.product(*param_grid.values())]
rmses = [] # Store the RMSEs for each params here
# Use cross validation to evaluate all parameters
for params in all_params:
m = Prophet(**params, holidays=holidays).fit(df_prophet) # Fit model with given params
df_cv = cross_validation(m, initial='730 days', period='365 days', horizon = '365 days', parallel="processes")
df_p = performance_metrics(df_cv, rolling_window=1)
rmses.append(df_p['rmse'].values[0])
# Find the best parameters
tuning_results = pd.DataFrame(all_params)
tuning_results['rmse'] = rmses
print(tuning_results)
# imprimindo os melhores parâmetros
best_params = all_params[np.argmin(rmses)]
print(best_params)
# serializando os melhores parâmetros
with open('best_params.pkl', 'wb') as file:
pickle.dump(best_params, file)
with open('m.pkl', 'wb') as file:
pickle.dump(m, file)
with open('cv.pkl', 'wb') as file:
pickle.dump(df_cv, file)
with open('df_p.pkl', 'wb') as file:
pickle.dump(df_p, file)
# exibindo o dataframe criado pela validação cruzada
df_cv
# exibindo dataframe com as métricas da validação cruzada
df_p = performance_metrics(df_cv)
df_p.head()
# exibindo a média das métricas
df_p.mean()
# visualizando gráfico com a métrica mape na validação cruzada
plot_cross_validation_metric(df_cv, metric='mape')
plt.show()
Com o modelo treinado, realizaremos previsão para os próximos 3 anos.
# criando dataframe com datas para previsão dos próximos três anos
future = m.make_future_dataframe(periods=36, freq='M')
future.tail()
# realizando predição
forecast = m.predict(future)
forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail()
# visualizando gráfico com os valores atuais, preditos e feriados
fig = go.Figure()
# Create and style traces
fig.add_trace(go.Scatter(x=df_prophet['ds'], y=df_prophet['y'], name='Atuais',))
fig.add_trace(go.Scatter(x=forecast['ds'], y=forecast['yhat'], name='Preditos',))
fig.add_trace(go.Scatter(x=forecast['ds'], y=forecast['holidays'], name='Feriados'))
fig.show()
fig.write_html("feriados.html")
Observa-se no gráfico acima que os feriados de Janeiro e Maio tem maior influência no consumo de energia.
# exibindo o gráfico com intervalo de incerteza
plot_plotly(m, forecast)
fig.write_html("incerteza.html")
# visualizando os componentes da previsão, incluindo: tendência, feriados e sazionalidade anual
fig2 = m.plot_components(forecast)
# criando cópia do dataframe com as previsões
forecast_copy = forecast
# Transformando os dados para a realidade utilizando o Box-Cox inverso
forecast_copy[['yhat','yhat_upper','yhat_lower']] = forecast[['yhat','yhat_upper','yhat_lower']].apply(lambda x: inv_boxcox(x, lam))
df_prophet['y'] = inv_boxcox(df_prophet['y'], lam)
# Exibindo os resultados
fig = go.Figure()
fig.add_trace(go.Scatter(x=df_prophet['ds'], y=df_prophet['y'], name='Atual',))
fig.add_trace(go.Scatter(x=forecast['ds'], y=forecast['yhat'], name='Previsto',))
fig.show()
fig.write_html("final.html")
A previsão da demanda de energia é uma tarefa com diversas incertezas, pois para gerar um modelo de previsão de demanda preciso, devem ser analisadas as questões de crescimento econômico, demográfico, de autoprodutor e tecnológico do país. Percebe-se ainda que influências externas de caráter político podem intervir no processo de modelagem e previsão do mercado de energia, não somente quando o processo de modelar e prever ocorre no âmbito do planejamento energético de um governo, como também no meio corporativo, quando o objetivo é prever e modelar o mercado de energia de uma empresa.
Ainda sim, o modelo de previsão utilizado neste projeto alcançou um MAPE - Erro Percentual Médio Absoluto de 7,82 %. A importância de prever a demanda de energia ao longo do tempo está totalmente ligada à escassez e à necessidade de geração. Um planejamento de distribuição adequado à demanda é importante para que a perda seja mínima, mesmo porque no país a energia elétrica ainda é cara para o consumidor devido aos custos operacionais.